/**
 * Copyright (c) 2012, Hakans Pirates - 
 * Johan Andersson spaiki17@gmail.com, 
 * Mattias Lundberg lundberg.mattias@gmail.com, 
 * Samuel Sjodin sjoodin@gmail.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the <organization> nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *  
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package pirates.network;

import com.jme3.math.Matrix3f;
import com.jme3.math.Quaternion;
import java.util.concurrent.Callable;

import pirates.PiratesServer;
import pirates.Settings;
import pirates.Settings.EntityTeam;
import pirates.Settings.EntityModels;
import pirates.Settings.ShipConstants;
import pirates.network.messages.*;

import com.jme3.math.Vector3f;
import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection;
import com.jme3.network.Message;
import com.jme3.network.MessageListener;
import com.jme3.network.Server;
import com.jme3.scene.Spatial;
import java.util.concurrent.LinkedBlockingQueue;
import pirates.CrateList;
import pirates.Utils;

import pirates.network.messages.ChatMessage;
import pirates.network.messages.ClientJoinMessage;
import pirates.network.messages.EntityMessage;
import pirates.crates.FloatingCrate;
import pirates.network.messages.HandshakeMessage;
import pirates.network.messages.HelloMessage;
import pirates.network.messages.ServerGiveEntityMessage;
import pirates.network.messages.ServerJoinMessage;
import pirates.notifications.Notify;
import pirates.ship.ShipControl;
import pirates.ship.cannon.CannonBall;

public class ServerNetListener
	implements
	MessageListener<HostedConnection>,
	ConnectionListener {

	com.jme3.network.Server server;
	PiratesServer app;

	public ServerNetListener(Server server, PiratesServer app) {
		this.server = server;
		this.server.addConnectionListener(this);
		this.server.addMessageListener(this);
		this.app = app;
	}

	@SuppressWarnings("unchecked")
	@Override
	public void messageReceived(final HostedConnection client, final Message msg) {
		// Notify.debug("message received from "+source.toString()+": "+msg);
		app.enqueue(new Callable() {

			@Override
			public Object call() throws Exception {
				if (msg instanceof HelloMessage) {
					HelloMessage message = (HelloMessage) msg;
					Notify.debug("Client at" + client.getAddress() + " says "
						+ message.getMessage());
					client.send(message);
				}
				if (msg instanceof HandshakeMessage) {
					HandshakeMessage message = (HandshakeMessage) msg;
					if (message.protocol_version != Settings.PROTOCOL_VERSION) {
						client.close("Different protocol versions. Client has v."
							+ message.protocol_version
							+ ", server has  v."
							+ Settings.PROTOCOL_VERSION);
					} else if (message.client_version != Settings.CLIENT_VERSION) {
						client.close("Different client versions. Client has v."
							+ message.client_version + ", server has  v."
							+ Settings.CLIENT_VERSION);
					} else if (message.server_version != Settings.SERVER_VERSION) {
						client.close("Different server versions. Client has v."
							+ message.server_version + ", server has  v."
							+ Settings.SERVER_VERSION);
					} else {
						// server and client has the same versions
						Notify.debug("Sending handshake message");
						client.send(new HandshakeMessage(
							Settings.PROTOCOL_VERSION,
							Settings.CLIENT_VERSION,
							Settings.SERVER_VERSION));
					}

				} else if (msg instanceof CrateMessage) {
					final CrateMessage m = (CrateMessage) msg;

					if (m.pickup) {
						app.enqueue(new Callable() {

							public Object call() throws Exception {
								app.getCrateNode().detachChildNamed(
									m.entityId);

								boolean removed = false;
								CrateList crts = app.getCrates();
								for (final Object o : crts) {
									final FloatingCrate c = (FloatingCrate) o;
									if (c.name.equals(m.entityId)) {
										((CrateList)crts).remove(c);
										removed = true;
										break;
									}
								}

								if (!removed) {
									client.send(new DenyMessage(m.id + "",
										"Nonexisting crate " + m.entityId, m));
								}
								server.broadcast(m);
								return null;
							}
						});

					} else {
						Spatial ship = app.ships.getShip(m.entityId);
						if (ship != null) {
							int balls = ship.<Integer>getUserData(ShipConstants.CANNON_BALLS.toString());
							if (balls < m.balls) {
								client.send(new DenyMessage(m.entityId,
									"Can't drop more balls then you have",
									m));
								return null;
							} else {
								ship.setUserData(
									ShipConstants.CANNON_BALLS.toString(),
									balls -= 5);
							}

							float health = ship.<Float>getUserData(ShipConstants.HITPOINTS.toString());
							if (health < m.health) {
								client.send(new DenyMessage(
									m.entityId,
									"Can't drop more hitpoints then you have",
									m));
								return null;
							} else {
								ship.setUserData(
									ShipConstants.HITPOINTS.toString(),
									health -= 5);
							}
						}
						FloatingCrate floatingCrate = new FloatingCrate(app,
							app.getCrateNode(), m.balls, m.health, m.pos,
							app.bulletAppState.getPhysicsSpace(),
							m.entityId);
						app.getCrates().add(floatingCrate);
						server.broadcast(m);
					}

				} else if (msg instanceof ClientJoinMessage) {
					ClientJoinMessage message = (ClientJoinMessage) msg;
					// check if player name exists - if it does, add a number in
					// the end
					// add player. (decide team, get ship type, new ship,
					// connect ship to player, add player to list)
					int id = 0;
					client.send(new ServerJoinMessage(0, 1, message.name, false)); // TODO:
					// improve

					ServerClientData.setPlayerId(client.getId(), message.name);
					ServerClientData.setConnected(client.getId(), true); // client
					// is
					// accepted
					// and
					// has
					// joined
					// game!

					// defining team
					EntityTeam team;
					EntityModels model;
					Vector3f spawnPoint;

					if (app.ships.weNeedMorePirates()) {
						team = EntityTeam.PIRATES;
						// spawnPoint = Settings.PirateSpawnPoint; //TODO: test
						// with Utils.getUntakenSpawnPoint(..)
						spawnPoint = Utils.getUntakenSpawnPoint(
							Settings.PirateSpawnPoint,
							Settings.SPAWN_AREA_RADIUS,
							app.ships.getShipsNode());
						Notify.debug("giving the " + team
							+ " ship spawn point: " + spawnPoint);
						model = EntityModels.BATTLESHIP;
					} else {
						team = EntityTeam.BRITISH;
						// spawnPoint = Settings.BrittishSpawnPoint;
						spawnPoint = Utils.getUntakenSpawnPoint(
							Settings.BrittishSpawnPoint,
							Settings.SPAWN_AREA_RADIUS,
							app.ships.getShipsNode());
						Notify.debug("giving the " + team
							+ " ship spawn point " + spawnPoint);
						model = EntityModels.BATTLESHIP;
					}

					app.ships.createShip(
						model.getValues().get(Settings.ShipConstants.MODEL_PATH.toString()).toString(), // TODO:
						// will
						// pirates
						// and
						// brittish
						// players
						// have
						// different
						// models?
						spawnPoint, message.name, model.getValues(), team.getValues());

					Spatial ship1 = app.ships.getShip(message.name);
					ShipControl ship = ship1.getControl(ShipControl.class);

					if (team == EntityTeam.PIRATES) {

						Matrix3f rotation = ship.getPhysicsRotationMatrix().mult(Utils.rotate90Z).mult(Utils.rotate90Z);
						// rotate the pirate boats 180 degrees so that the
						// trades ships passes in front of them
						ship.setPhysicsRotation(rotation);
						Notify.debug("ROTATING PIRATE SHIP TO "
							+ rotation);

					} else {
					}
					Notify.debug("sending ServerGiveEntityMessage");

					ServerGiveEntityMessage msg = new ServerGiveEntityMessage(
						ship1.getName(), // name / id
						ship1.<String>getUserData(ShipConstants.TEAM.toString()).toString(), // entityTeam
						ship1.<String>getUserData(
						ShipConstants.ENTITY_MODEL.toString()).toString(), // entityType
						(Float) ship1.<Float>getUserData(ShipConstants.HITPOINTS.toString()), // hitpoints
						ship.getPhysicsLocation(), ship.getPhysicsRotation(), ship.getVelocity(),
						ship.getSteer());
					client.send(msg);
					Notify.debug("Player " + message.name
						+ " is now joining the game!");

					// calculate if we have enough players to start the game -
					// if we do, start the scenario
					if (!Settings.SCENARIO_HAS_STARTED) {
						if (app.ships.getNrOfBrittish() >= 1
							&& app.ships.getNrOfPirates() >= 1) {
							app.startScenario();
						}
					}

					for (Object obj : app.getCrates()) {
						FloatingCrate fc = (FloatingCrate) obj;
						client.send(new CrateMessage(fc.name, fc.id,
							fc.crateModel.getLocalTranslation(),
							fc.numHealt, fc.numCannonBalls, false, "null"));
					}

				} else if (msg instanceof ChatMessage) {
					ChatMessage mess = (ChatMessage) msg;
					server.broadcast(mess);
					Notify.debug("Chat message: " + mess.text + " from "
						+ mess.name);
				} else if (msg instanceof CannonBallMessage) {
					CannonBallMessage message = (CannonBallMessage) msg;
					Notify.debug("Cannonball from " + message.entityId
						+ "");
					if (app.ships.getShip(message.entityId) == null) {
						return null;
					}
					Spatial tmpship = app.ships.getShip(message.entityId);
					int balls = tmpship.<Integer>getUserData(ShipConstants.CANNON_BALLS.toString());

					if (!Utils.close(message.position,
						tmpship.getLocalTranslation(), 200)) {
						client.send(new DenyMessage(message.entityId,
							"Must shoot close to your position.", message));
						return null;
					} else if (balls <= 0) {
						client.send(new DenyMessage(message.entityId,
							"No balls left.", message));
						return null;
					}

					tmpship.setUserData(ShipConstants.CANNON_BALLS.toString(),
						--balls);

                                        server.broadcast(msg);
					CannonBall c = new CannonBall(app, tmpship.getControl(ShipControl.class), 
                                                app.bulletAppState.getPhysicsSpace(), app.getRootNode());
					c.shoot(message.position, message.velocity);
				} else if (msg instanceof EntityMessage) {
					EntityMessage message = (EntityMessage) msg;
					switch (EntityModels.valueOf(message.entityType)) {
						case BATTLESHIP:
							if (!handleEntityUpdate(message)) {
								return null;
							}
							break;
						case FLAGSHIP:
							if (!handleEntityUpdate(message)) {
								return null;
							}
							break;
						default:
							Notify.debug("Warning: Recieved message from server "
								+ "with unexisting entity type: "
								+ message.entityType);
							break;
					} // <-end of switch
					server.broadcast(message);
				}
				return null;
			}

			private boolean handleEntityUpdate(EntityMessage message) {
				Spatial ship = app.ships.getShip(message.entityId);
				if (ship != null) {
					// does the ship exist?
					// yes: update its values
					// if(!((String)
					// ship.<String>getUserData(ShipConstants.TEAM.toString())).contains(message.entityTeam)){
					// //entity has changed team. possible? take action?
					// }
					float hp = ship.<Float>getUserData(ShipConstants.HITPOINTS.toString());
					if (hp != message.hitpoints) {
						// change of hitpoint. update
						ship.setUserData(ShipConstants.HITPOINTS.toString(),
							message.hitpoints);
					}
					ShipControl shipControl = app.ships.getShip(
						message.entityId).getControl(ShipControl.class);
					if (!Utils.close(message.position,
						ship.getLocalTranslation(), 200)) {
						DenyMessage deny = new DenyMessage(message.entityId,
							"Must move close to your position.", message);
						deny.moveHere = shipControl.getPhysicsLocation();
						client.send(deny);
						return false;
					}
					float maxSpeed = ship.<Float>getUserData(ShipConstants.MAX_SPEED.toString());
					if (message.velocity > maxSpeed) {
						DenyMessage deny = new DenyMessage(message.entityId,
							"Moving to fast.", message);
						deny.moveHere = shipControl.getPhysicsLocation();
						client.send(deny);
						return false;
					}
					shipControl.predict(message.position, message.velocity,
						message.rotation, message.angularVelocity,
						message.timeStamp);
				} else {
					// no, ship does not exist locally. reject update. the
					// server is always right.
					client.send(new DenyMessage());
					// client.send(new DenyMessage(message.entityId,
					// "rejecting entity update. Entity "
					// + message.entityType + " with id "
					// + message.otherEntityId
					// + " does not exist on server",
					// message));
					return false;
				}
				return true;
			}
		});

	}

	@Override
	public void connectionAdded(final Server server, HostedConnection client) {
		int clientId = (int) client.getId();
		if (!ServerClientData.exsists(clientId)) {
			ServerClientData.add(clientId);
			Notify.debug("new connection: " + clientId);
			app.enqueue(new Callable() {

				@Override
				public Object call() throws Exception {
					server.broadcast(new HelloMessage("hej!"));
					return null;
				}
			});

			Notify.debug("sending new hej to client at " + client.getAddress());

		} else {
			Notify.stdout("Client ID exists!");
			return;
		}
	}

	@Override
	public void connectionRemoved(Server server, HostedConnection client) {
		final int clientId = (int) client.getId();
		final String playerId = ServerClientData.getPlayerId(clientId);
		Spatial ship = app.ships.getShip(playerId);
		if (ship != null) {
			Notify.debug("Sinking ship..");
			app.ships.removeShip(playerId);
			ship.setUserData(ShipConstants.HITPOINTS.toString(), 0);

			EntityMessage msg = new EntityMessage(playerId,
				ship.<String>getUserData(ShipConstants.TEAM.toString()) + "",
				ship.<String>getUserData(ShipConstants.ENTITY_MODEL.toString())
				+ "", 0f, Vector3f.ZERO, Quaternion.IDENTITY, 0f,
				0f);

			server.broadcast(msg);
		}
		ServerClientData.remove(clientId);
		Notify.stdout("Removed connection: " + clientId);

		/*
		 * app.enqueue(new Callable<Void>() {
		 * 
		 * public Void call() throws Exception { String name =
		 * PlayerData.getStringData(playerId, "name");
		 * worldManager.removePlayer(playerId); server.broadcast(new
		 * ChatMessage("Server", name + " left the game"));
		 * Logger.getLogger(ServerNetListener.class.getName()).log(Level.INFO,
		 * "Broadcast player left message"); if
		 * (PlayerData.getHumanPlayers().isEmpty()) { gameManager.stopGame(); }
		 * return null; } });
		 */
	}
}
